import { mat4, vec3 } from "wgpu-matrix";

import {
  cubeVertexArray,
  cubeVertexSize,
  cubeUVOffset,
  cubePositionOffset,
  cubeVertexCount,
} from "../../meshes/cube";

import basicVertWGSL from "../../shaders/basic.vert.wgsl";
import sampleCubemapWGSL from "./sampleCubemap.frag.wgsl";
import { initWebGpu } from "../../utils/gputools";
import type { Base } from "../Base";

export default class CubeMap implements Base {
  name = "CubeMap";
  description =
    "This example shows how to render and sample from a cubemap texture. \
    Cubemap image available under a Creative Commons Attribution 3.0 Unported License at https://www.humus.name/index.php?page=Textures&ID=58";
  async run(canvas: HTMLCanvasElement) {
    const result = await initWebGpu(canvas);
    if (!result) {
      console.error("Failed to initialize WebGPU.");
      return;
    }
    const { device, context, presentationFormat } = result;
    // Create a vertex buffer from the cube data.
    const verticesBuffer = device.createBuffer({
      size: cubeVertexArray.byteLength,
      usage: GPUBufferUsage.VERTEX,
      mappedAtCreation: true,
    });
    new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray);
    verticesBuffer.unmap();

    const pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: {
        module: device.createShaderModule({
          code: basicVertWGSL,
        }),
        buffers: [
          {
            arrayStride: cubeVertexSize,
            attributes: [
              {
                // position
                shaderLocation: 0,
                offset: cubePositionOffset,
                format: "float32x4",
              },
              {
                // uv
                shaderLocation: 1,
                offset: cubeUVOffset,
                format: "float32x2",
              },
            ],
          },
        ],
      },
      fragment: {
        module: device.createShaderModule({
          code: sampleCubemapWGSL,
        }),
        targets: [
          {
            format: presentationFormat,
          },
        ],
      },
      primitive: {
        topology: "triangle-list",

        // Since we are seeing from inside of the cube
        // and we are using the regular cube geomtry data with outward-facing normals,
        // the cullMode should be 'front' or 'none'.
        cullMode: "front",
      },

      // Enable depth testing so that the fragment closest to the camera
      // is rendered in front.
      depthStencil: {
        depthWriteEnabled: true,
        depthCompare: "less",
        format: "depth24plus",
      },
    });

    const depthTexture = device.createTexture({
      size: [canvas.width, canvas.height],
      format: "depth24plus",
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });

    // Fetch the 6 separate images for negative/positive x, y, z axis of a cubemap
    // and upload it into a GPUTexture.
    let cubemapTexture: GPUTexture;
    {
      // The order of the array layers is [+X, -X, +Y, -Y, +Z, -Z]
      const imgSrcs = [
        "../src/assets/imgs/cubemap/posx.jpg",
        "../src/assets/imgs/cubemap/negx.jpg",
        "../src/assets/imgs/cubemap/posy.jpg",
        "../src/assets/imgs/cubemap/negy.jpg",
        "../src/assets/imgs/cubemap/posz.jpg",
        "../src/assets/imgs/cubemap/negz.jpg",
      ];
      const promises = imgSrcs.map(async (src) => {
        const response = await fetch(src);
        return createImageBitmap(await response.blob());
      });
      const imageBitmaps = await Promise.all(promises);

      cubemapTexture = device.createTexture({
        dimension: "2d",
        textureBindingViewDimension: "cube",
        // Create a 2d array texture.
        // Assume each image has the same size.
        size: [imageBitmaps[0].width, imageBitmaps[0].height, 6],
        format: "rgba8unorm",
        usage:
          GPUTextureUsage.TEXTURE_BINDING |
          GPUTextureUsage.COPY_DST |
          GPUTextureUsage.RENDER_ATTACHMENT,
      });

      for (let i = 0; i < imageBitmaps.length; i++) {
        const imageBitmap = imageBitmaps[i];
        device.queue.copyExternalImageToTexture(
          { source: imageBitmap },
          { texture: cubemapTexture, origin: [0, 0, i] },
          [imageBitmap.width, imageBitmap.height]
        );
      }
    }

    const uniformBufferSize = 4 * 16; // 4x4 matrix
    const uniformBuffer = device.createBuffer({
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    const sampler = device.createSampler({
      magFilter: "linear",
      minFilter: "linear",
    });

    const uniformBindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        {
          binding: 0,
          resource: {
            buffer: uniformBuffer,
            offset: 0,
            size: uniformBufferSize,
          },
        },
        {
          binding: 1,
          resource: sampler,
        },
        {
          binding: 2,
          resource: cubemapTexture.createView({
            dimension: "cube",
          }),
        },
      ],
    });

    const renderPassDescriptor: GPURenderPassDescriptor = {
      colorAttachments: [
        {
          view: undefined as unknown as GPUTextureView, // Assigned later
          loadOp: "clear",
          storeOp: "store",
        },
      ],
      depthStencilAttachment: {
        view: depthTexture.createView(),

        depthClearValue: 1.0,
        depthLoadOp: "clear",
        depthStoreOp: "store",
      },
    };

    const aspect = canvas.width / canvas.height;
    const projectionMatrix = mat4.perspective(
      (2 * Math.PI) / 5,
      aspect,
      1,
      3000
    );

    const modelMatrix = mat4.scaling(vec3.fromValues(1000, 1000, 1000));
    const modelViewProjectionMatrix = mat4.create();
    const viewMatrix = mat4.identity();

    const tmpMat4 = mat4.create();

    // Comppute camera movement:
    // It rotates around Y axis with a slight pitch movement.
    function updateTransformationMatrix() {
      const now = Date.now() / 800;

      mat4.rotate(
        viewMatrix,
        vec3.fromValues(1, 0, 0),
        (Math.PI / 10) * Math.sin(now),
        tmpMat4
      );
      mat4.rotate(tmpMat4, vec3.fromValues(0, 1, 0), now * 0.2, tmpMat4);

      mat4.multiply(tmpMat4, modelMatrix, tmpMat4);
      mat4.multiply(
        projectionMatrix,
        tmpMat4,
        modelViewProjectionMatrix
      );
    }

    function frame() {
      updateTransformationMatrix();
      device.queue.writeBuffer(
        uniformBuffer,
        0,
        modelViewProjectionMatrix.buffer,
        modelViewProjectionMatrix.byteOffset,
        modelViewProjectionMatrix.byteLength
      );

      // @ts-ignore
      renderPassDescriptor.colorAttachments[0].view = context
        .getCurrentTexture()
        .createView();

      const commandEncoder = device.createCommandEncoder();
      const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
      passEncoder.setPipeline(pipeline);
      passEncoder.setVertexBuffer(0, verticesBuffer);
      passEncoder.setBindGroup(0, uniformBindGroup);
      passEncoder.draw(cubeVertexCount);
      passEncoder.end();
      device.queue.submit([commandEncoder.finish()]);

      requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
  }

  destroy(): void {
    // to do
  }
}
